From fdef3c0d0cb3330a08666f98a77455c45dfc2630 Mon Sep 17 00:00:00 2001 From: Dirk Brenken Date: Thu, 11 Dec 2025 22:48:08 +0100 Subject: [PATCH] luci-app-travelmate: sync with travelmate 2.3.0-1 Signed-off-by: Dirk Brenken --- applications/luci-app-travelmate/Makefile | 2 + .../resources/view/travelmate/logtemplate.js | 64 +++-- .../resources/view/travelmate/overview.js | 231 ++++++++++-------- .../luci/menu.d/luci-app-travelmate.json | 2 +- .../share/rpcd/acl.d/luci-app-travelmate.json | 8 +- 5 files changed, 178 insertions(+), 129 deletions(-) diff --git a/applications/luci-app-travelmate/Makefile b/applications/luci-app-travelmate/Makefile index 19cc8f5e30..4ad673344f 100644 --- a/applications/luci-app-travelmate/Makefile +++ b/applications/luci-app-travelmate/Makefile @@ -6,6 +6,8 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI support for Travelmate LUCI_DEPENDS:=+luci-base +luci-lib-uqr +travelmate +PKG_VERSION:=2.3.0 +PKG_RELEASE:=1 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Dirk Brenken diff --git a/applications/luci-app-travelmate/htdocs/luci-static/resources/view/travelmate/logtemplate.js b/applications/luci-app-travelmate/htdocs/luci-static/resources/view/travelmate/logtemplate.js index 23a8fbebe7..10c552721f 100644 --- a/applications/luci-app-travelmate/htdocs/luci-static/resources/view/travelmate/logtemplate.js +++ b/applications/luci-app-travelmate/htdocs/luci-static/resources/view/travelmate/logtemplate.js @@ -1,33 +1,54 @@ 'use strict'; -'require fs'; +'require rpc'; + +const callLogRead = rpc.declare({ + object: 'log', + method: 'read', + params: ['lines', 'stream', 'oneshot'], + expect: {} +}); function Logview(logtag, name) { return L.view.extend({ - load: function() { - return Promise.all([ - L.resolveDefault(fs.stat('/sbin/logread'), null), - L.resolveDefault(fs.stat('/usr/sbin/logread'), null) - ]); - }, + load: () => Promise.resolve(), - render: function(stat) { - let logger = stat[0]?.path || stat[1]?.path || null; + render: () => { + L.Poll.add(() => { + return callLogRead(1000, false, true).then(res => { + const logEl = document.getElementById('logfile'); + if (!logEl) return; - if (!logger) { - return E('div', { class: 'error' }, _('logread not found on system.')); - } - L.Poll.add(function() { - return L.resolveDefault(fs.exec_direct(logger, ['-e', logtag])).then(function(res) { - var log = document.getElementById('logfile'); - if (log) { - log.value = res ? res.trim() : _('No %s related logs yet!').format(name); - log.scrollTop = log.scrollHeight; + const entries = res?.log ?? []; + if (entries.length > 0) { + const filtered = entries + .filter(entry => !logtag || entry.msg.includes(logtag)) + .map(entry => { + const d = new Date(entry.time); + const date = d.toLocaleDateString([], { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + const time = d.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + return `[${date}-${time}] ${entry.msg}`; + }); + logEl.value = filtered.join('\n'); + } else { + logEl.value = _('No %s related logs yet!').format(name); } + logEl.scrollTop = logEl.scrollHeight; }); }); + return E('div', { class: 'cbi-map' }, [ E('div', { class: 'cbi-section' }, [ - E('div', { class: 'cbi-section-descr' }, _('The syslog output, pre-filtered for messages related to: %s').format(name)), + E('div', { class: 'cbi-section-descr' }, + _('The syslog output, pre-filtered for messages related to: %s').format(name)), E('textarea', { id: 'logfile', style: 'min-height: 500px; max-height: 90vh; width: 100%; padding: 5px; font-family: monospace; resize: vertical;', @@ -37,12 +58,11 @@ function Logview(logtag, name) { ]) ]); }, + handleSaveApply: null, handleSave: null, handleReset: null }); } -return L.Class.extend({ - Logview: Logview -}); +return L.Class.extend({ Logview }); diff --git a/applications/luci-app-travelmate/htdocs/luci-static/resources/view/travelmate/overview.js b/applications/luci-app-travelmate/htdocs/luci-static/resources/view/travelmate/overview.js index 15df17ebf4..84c17852b3 100644 --- a/applications/luci-app-travelmate/htdocs/luci-static/resources/view/travelmate/overview.js +++ b/applications/luci-app-travelmate/htdocs/luci-static/resources/view/travelmate/overview.js @@ -1,4 +1,5 @@ 'use strict'; +'require dom'; 'require view'; 'require poll'; 'require fs'; @@ -14,53 +15,63 @@ */ function handleAction(ev) { let ifaceValue; - if (ev === 'restart') { + if (ev === 'restartInterface') { ifaceValue = String(uci.get('travelmate', 'global', 'trm_iface') || 'trm_wwan'); return fs.exec('/etc/init.d/travelmate', ['stop']) .then(fs.exec('/sbin/ifup', [ifaceValue])) .then(fs.exec('/etc/init.d/travelmate', ['start'])) } + if (ev === 'restartTravelmate') { + const map = document.querySelector('.cbi-map'); + return dom.callClassMethod(map, 'save') + .then(L.bind(ui.changes.apply, ui.changes)) + .then(function () { + return fs.exec_direct('/etc/init.d/travelmate', ['restart']); + }) + } if (ev === 'setup') { ifaceValue = String(uci.get('travelmate', 'global', 'trm_iface') || ''); L.ui.showModal(_('Interface Wizard'), [ E('p', _('To use Travelmate, you have to set up an uplink interface once. This wizard creates an IPv4- and an IPv6 alias network interface with all required network- and firewall settings.')), E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [ - E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em' }, [ - E('input', { 'class': 'cbi-input-text', 'id': 'iface', 'placeholder': 'trm_wwan', 'value': ifaceValue, 'maxlength': '15', 'spellcheck': 'false' }), - '\xa0\xa0\xa0', + E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em;' }, [ + E('input', { 'class': 'cbi-input-text', 'id': 'iface', 'placeholder': 'trm_wwan', 'value': ifaceValue, 'maxlength': '15', 'spellcheck': 'false', style: 'margin-right:.5em;' }), _('The uplink interface name') ]), - E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em' }, [ - E('input', { 'class': 'cbi-input-text', 'id': 'zone', 'placeholder': 'wan', 'maxlength': '15', 'spellcheck': 'false' }), - '\xa0\xa0\xa0', + E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em;' }, [ + E('input', { 'class': 'cbi-input-text', 'id': 'zone', 'placeholder': 'wan', 'maxlength': '15', 'spellcheck': 'false', style: 'margin-right:.5em;' }), _('The firewall zone name') ]), - E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em' }, [ - E('input', { 'class': 'cbi-input-text', 'id': 'metric', 'placeholder': '100', 'maxlength': '3', 'spellcheck': 'false' }), - '\xa0\xa0\xa0', + E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em;' }, [ + E('input', { 'class': 'cbi-input-text', 'id': 'metric', 'placeholder': '100', 'maxlength': '3', 'spellcheck': 'false', style: 'margin-right:.5em;' }), _('The interface metric') ]) ]), E('div', { 'class': 'right' }, [ E('button', { 'class': 'cbi-button', + 'style': 'float:none;margin-right:.4em;', 'click': L.hideModal }, _('Dismiss')), - ' ', E('button', { 'class': 'cbi-button cbi-button-positive important', + 'style': 'float:none', 'click': ui.createHandlerFn(this, function (ev) { - let iface = document.getElementById('iface').value || 'trm_wwan', - zone = document.getElementById('zone').value || 'wan', - metric = document.getElementById('metric').value || '100'; - fs.exec('/etc/init.d/travelmate', ['setup', iface, zone, metric]).then(function (rc) { - L.hideModal(); - if (rc.code == 1) { - ui.addNotification(null, E('p', _('Setup failed, the interface already exists!')), 'error'); - } else { - location.reload(); - } - }); + const iface = (document.getElementById('iface').value || 'trm_wwan').toLowerCase(); + const zone = (document.getElementById('zone').value || 'wan').toLowerCase(); + const metric = document.getElementById('metric').value.replace(/\D/g, '') || '100'; + fs.exec('/etc/init.d/travelmate', ['setup', iface, zone, metric]) + .then(function (rc) { + L.hideModal(); + switch (rc.code) { + case 1: + ui.addNotification(null, E('p', _('The interface already exists!')), 'info'); + break; + default: + location.reload(); + break; + } + }) }) }, _('Save')) ]) @@ -70,20 +81,20 @@ function handleAction(ev) { if (ev === 'qrcode') { return Promise.all([ - uci.load('wireless') - ]).then(function () { - let w_sid, w_device, w_ssid, w_enc, w_key, w_hidden, result, - w_sections = uci.sections('wireless', 'wifi-iface'), - optionsAP = [E('option', { value: '' }, [_('-- AP Selection --')])]; - for (let i = 0; i < w_sections.length; i++) { - if (w_sections[i].mode === 'ap' && w_sections[i].disabled !== '1') { - w_sid = i; - w_device = w_sections[i].device; - w_ssid = w_sections[i].ssid; - optionsAP.push(E('option', { value: w_sid }, w_device + ', ' + w_ssid)); + uci.load('wireless')]) + .then(function () { + let w_sid, w_device, w_ssid, w_enc, w_key, w_hidden, result; + const w_sections = uci.sections('wireless', 'wifi-iface'); + const optionsAP = [E('option', { value: '' }, [_('-- AP Selection --')])]; + for (let i = 0; i < w_sections.length; i++) { + if (w_sections[i].mode === 'ap' && w_sections[i].disabled !== '1') { + w_sid = i; + w_device = w_sections[i].device; + w_ssid = w_sections[i].ssid; + optionsAP.push(E('option', { value: w_sid }, w_device + ', ' + w_ssid)); + } } - } - let selectAP = E('select', { + let selectAP = E('select', { id: 'selectID', class: 'cbi-input-select', change: function (ev) { @@ -100,16 +111,17 @@ function handleAction(ev) { } else { w_enc = 'WPA'; } - let data = `WIFI:S: ${w_ssid};T: ${w_enc};P: ${w_key};H: ${w_hidden};;`; + const data = `WIFI:S:${w_ssid};T:${w_enc};P:${w_key};H:${w_hidden};;`; const options = { - pixelSize: 4, + pixelSize: 12, + margin: 1, + ecLevel: 'M', whiteColor: 'white', blackColor: 'black' }; - let svg = uqr.renderSVG(data, options); + const svg = uqr.renderSVG(data, options); result.innerHTML = svg.trim(); - } - else { + } else { result.textContent = ''; } } @@ -117,18 +129,15 @@ function handleAction(ev) { L.ui.showModal(_('QR-Code Overview'), [ E('p', _('Render the QR-Code of the selected Access Point to transfer the WLAN credentials to your mobile devices comfortably.')), E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [ - E('label', { 'class': 'cbi-input-select', 'style': 'padding-top:.5em' }, [ - selectAP, - ]) + E('label', { 'class': 'cbi-input-select', 'style': 'padding-top:.5em' }, [selectAP,]) ]), - '\xa0', E('div', { 'id': 'qrcode' }), E('div', { 'class': 'right' }, [ E('button', { 'class': 'cbi-button', - 'click': L.hideModal + 'click': L.hideModal }, _('Dismiss')) ]) ]); @@ -141,7 +150,7 @@ return view.extend({ return Promise.all([ uci.load('travelmate'), network.getWifiDevices().then(function (res) { - let radios = []; + const radios = []; for (let i = 0; i < res.length; i++) { radios.push(res[i].sid); } @@ -160,18 +169,28 @@ return view.extend({ For further information check the online documentation.
\ Please note: On first start please call the \'Interface Wizard\' once, to make the necessary network- and firewall settings.')); + /* + set text content helper function + */ + const setText = (id, value) => { + const el = document.getElementById(id); + if (el) { + el.textContent = value || '-'; + } + }; + /* poll runtime information */ pollData: poll.add(function () { return L.resolveDefault(fs.stat('/tmp/trm_runtime.json'), null).then(function (res) { - let status = document.getElementById('status'); + const status = document.getElementById('status'); if (res && res.size > 0) { L.resolveDefault(fs.read_direct('/tmp/trm_runtime.json'), null).then(function (res) { if (res) { let info = JSON.parse(res); if (status && info) { - status.textContent = (info.data.travelmate_status || '-') + ' / ' + (info.data.travelmate_version || '-'); + status.textContent = `${info.data.travelmate_status || '-'} (frontend: ${info.data.frontend_ver || '-'} / backend: ${info.data.backend_ver || '-'})`; if (info.data.travelmate_status.startsWith('running')) { if (!status.classList.contains("spinning")) { status.classList.add("spinning"); @@ -187,33 +206,15 @@ return view.extend({ status.classList.remove("spinning"); } } - let station_id = document.getElementById('station_id'); - if (station_id && info) { - station_id.textContent = info.data.station_id || '-'; - } - let station_mac = document.getElementById('station_mac'); - if (station_mac && info) { - station_mac.textContent = info.data.station_mac || '-'; - } - let station_interfaces = document.getElementById('station_interfaces'); - if (station_interfaces && info) { - station_interfaces.textContent = info.data.station_interfaces || '-'; - } - let station_subnet = document.getElementById('station_subnet'); - if (station_subnet && info) { - station_subnet.textContent = info.data.station_subnet || '-'; - } - let run_flags = document.getElementById('run_flags'); - if (run_flags && info) { - run_flags.textContent = info.data.run_flags || '-'; - } - let ext_hooks = document.getElementById('ext_hooks'); - if (ext_hooks && info) { - ext_hooks.textContent = info.data.ext_hooks || '-'; - } - let run = document.getElementById('run'); - if (run && info) { - run.textContent = info.data.last_run || '-'; + if (info) { + setText('station_id', info.data.station_id); + setText('station_mac', info.data.station_mac); + setText('station_interfaces', info.data.station_interfaces); + setText('station_subnet', info.data.station_subnet); + setText('run_flags', info.data.run_flags); + setText('ext_hooks', info.data.ext_hooks); + setText('run', info.data.last_run); + setText('sys', info.data.system); } } }); @@ -265,29 +266,9 @@ return view.extend({ E('label', { 'class': 'cbi-value-title', 'style': 'margin-bottom:-5px;padding-top:0rem;' }, _('Last Run')), E('div', { 'class': 'cbi-value-field', 'id': 'run', 'style': 'margin-bottom:-5px;color:#37c;' }, '-') ]), - E('div', { class: 'right' }, [ - E('button', { - 'class': 'cbi-button cbi-button-apply', - 'style': 'float:none;margin-right:.4em;', - 'id': 'btn_suspend', - 'click': ui.createHandlerFn(this, function () { - return handleAction('qrcode'); - }) - }, [_('AP QR-Codes...')]), - E('button', { - 'class': 'cbi-button cbi-button-negative', - 'style': 'float:none;margin-right:.4em;', - 'click': ui.createHandlerFn(this, function () { - return handleAction('restart'); - }) - }, [_('Restart Interface')]), - E('button', { - 'class': 'cbi-button cbi-button-negative', - 'style': 'float:none;', - 'click': ui.createHandlerFn(this, function () { - return handleAction('setup'); - }) - }, [_('Interface Wizard...')]) + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title', 'style': 'margin-bottom:-5px;padding-top:0rem;' }, _('System Info')), + E('div', { 'class': 'cbi-value-field', 'id': 'sys', 'style': 'margin-bottom:-5px;color:#37c;' }, '-') ]) ]); }, o, this); @@ -308,6 +289,12 @@ return view.extend({ o = s.taboption('general', form.Flag, 'trm_enabled', _('Enabled'), _('Enable the travelmate service.')); o.rmempty = false; + o = s.taboption('general', widgets.NetworkSelect, 'trm_iface', _('WWAN Interface'), _('Select an existing wireless WAN network interface or create a new one with the \'Interface Wizard\'.')); + o.multiple = false; + o.nocreate = true; + o.optional = true; + o.rmempty = true; + o = s.taboption('general', form.MultiValue, 'trm_radio', _('Radio Selection'), _('Restrict travelmate to certain radio\(s\).')); for (let i = 0; i < result[1].length; i++) { o.value(result[1][i]); @@ -315,7 +302,7 @@ return view.extend({ o.placeholder = _('-- default --'); o.optional = true; o.rmempty = true; - o.write = function(section_id, value) { + o.write = function (section_id, value) { uci.set('travelmate', section_id, 'trm_radio', value.join(' ')); }; @@ -456,10 +443,10 @@ return view.extend({ */ o = s.taboption('adv_email', form.DummyValue, '_sub'); o.rawhtml = true; - o.default = '' + _('E-Mail notifications require the separate setup of the msmtp package.') + '' - + '
' + o.default = '' + _('Changes on this tab needs a travelmate service restart to take effect.') + '' + + '
'; - o = s.taboption('adv_email', form.Flag, 'trm_mail', _('E-Mail Hook'), _('Sends notification E-Mails after every succesful uplink connect.')); + o = s.taboption('adv_email', form.Flag, 'trm_mail', _('E-Mail Notification'), _('Sends notification E-Mails after every succesful uplink connect.')); o.rmempty = false; o = s.taboption('adv_email', form.Value, 'trm_mailreceiver', _('E-Mail Receiver Address'), _('Receiver address for travelmate notification E-Mails.')); @@ -482,7 +469,47 @@ return view.extend({ o.placeholder = 'trm_notify'; o.rmempty = true; + s = m.section(form.NamedSection, 'global'); + s.render = L.bind(function () { + return E('div', { 'class': 'cbi-page-actions' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-negative important', + 'style': 'float:none;margin-right:.4em;', + 'title': 'Interface Setup', + 'click': ui.createHandlerFn(this, function () { + return handleAction('setup'); + }) + }, [_('Interface Wizard...')]), + E('button', { + 'class': 'btn cbi-button cbi-button-negative important', + 'style': 'float:none;margin-right:.4em;', + 'title': 'Restart Interface', + 'click': ui.createHandlerFn(this, function () { + return handleAction('restartInterface'); + }) + }, [_('Interface Restart')]), + E('button', { + 'class': 'btn cbi-button cbi-button-apply important', + 'style': 'float:none;margin-right:.4em;', + 'title': 'QRCode', + 'id': 'btn_suspend', + 'click': ui.createHandlerFn(this, function () { + return handleAction('qrcode'); + }) + }, [_('AP QR-Codes...')]), + E('button', { + 'class': 'btn cbi-button cbi-button-positive important', + 'style': 'float:none;margin-right:.4em;', + 'title': 'Save & Restart', + 'click': function () { + return handleAction('restartTravelmate'); + } + }, [_('Save & Restart')]) + ]) + }); return m.render(); }, + handleSaveApply: null, + handleSave: null, handleReset: null }); diff --git a/applications/luci-app-travelmate/root/usr/share/luci/menu.d/luci-app-travelmate.json b/applications/luci-app-travelmate/root/usr/share/luci/menu.d/luci-app-travelmate.json index 79a0299425..660e7db9fc 100644 --- a/applications/luci-app-travelmate/root/usr/share/luci/menu.d/luci-app-travelmate.json +++ b/applications/luci-app-travelmate/root/usr/share/luci/menu.d/luci-app-travelmate.json @@ -9,7 +9,7 @@ "depends": { "acl": [ "luci-app-travelmate" ], "fs": { - "/usr/bin/travelmate.sh": "executable", + "/usr/bin/travelmate-service.sh": "executable", "/etc/init.d/travelmate": "executable" }, "uci": { "travelmate": true } diff --git a/applications/luci-app-travelmate/root/usr/share/rpcd/acl.d/luci-app-travelmate.json b/applications/luci-app-travelmate/root/usr/share/rpcd/acl.d/luci-app-travelmate.json index 35fe60aeb9..2642282dfd 100644 --- a/applications/luci-app-travelmate/root/usr/share/rpcd/acl.d/luci-app-travelmate.json +++ b/applications/luci-app-travelmate/root/usr/share/rpcd/acl.d/luci-app-travelmate.json @@ -15,15 +15,15 @@ "/var/run/travelmate.scan": [ "read" ], "/var/state/travelmate.refresh": [ "read" ], "/tmp/trm_runtime.json": [ "read" ], - "/sbin/logread -e trm-": [ "exec" ], - "/usr/sbin/logread -e trm-": [ "exec" ], "/sbin/ifup *": [ "exec" ], "/etc/init.d/travelmate start" : [ "exec" ], + "/etc/init.d/travelmate restart" : [ "exec" ], "/etc/init.d/travelmate stop" : [ "exec" ], "/etc/init.d/travelmate setup [0-9a-z_]* [0-9a-z_]* [0-9]*" : [ "exec" ], - "/etc/init.d/travelmate scan radio[0-1]" : [ "exec" ] + "/etc/init.d/travelmate scan radio[0-9]" : [ "exec" ] }, - "uci": [ "travelmate", "wireless" ] + "uci": [ "travelmate", "wireless" ], + "log": [ "read" ] } } } -- 2.30.2